<?php

/*
Copyright 2009-2011 Sam Weiss
All Rights Reserved.

This file is part of Spark/Plug.

Spark/Plug is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

if (!defined('spark/plug'))
{
	header('HTTP/1.1 403 Forbidden');
	exit('<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"><html><head><title>403 Forbidden</title></head><body><h1>Forbidden</h1><p>You don\'t have permission to access the requested resource on this server.</p></body></html>');
}

define('spark_plug_version', '1.0.0');

/**
 * The SparkException class is our custom exception class.
 *
 * @package Spark/Plug
 */
// -----------------------------------------------------------------------------

class SparkException extends Exception
{
	private $_vars;
	
   /**
    * Construct a new exception.
    *
    * @param string $message
    * @param int $code
    * @return SparkException object
    */
	public function __construct($message, $code = 0, $vars = NULL)
	{
		parent::__construct($message, $code);
		$this->_vars = empty($vars) ? array() : $vars;
	}

   /**
    * Return vars associated with this exception.
    *
    * @return array
    */
	public function vars()
	{
		return $this->_vars;
	}
}


/**
 * The SparkHTTPException class is used to automatically convert HTTP status codes to exceptions.
 *
 * @package Spark/Plug
 */
// -----------------------------------------------------------------------------

class SparkHTTPException extends SparkException
{
	private $_httpStatusCode;
	private $_httpStatusText;
	
   /**
    * Convert HTTP status code to equivalent exception.
    *
    * @param string $message
    * @param int $code
    * @return SparkException object
    */
	public function __construct($httpStatusCode, $httpStatusText, $message = '', $vars = '')
	{
		parent::__construct($message, 0, $vars);
		$this->_httpStatusCode = $httpStatusCode;
		$this->_httpStatusText = $httpStatusText;
	}

   /**
    * Return HTTP status code associated with this exception.
    *
    * @return int
    */
	public function getHTTPStatusCode()
	{
		return $this->_httpStatusCode;
	}
	
   /**
    * Return HTTP status text associated with this exception.
    *
    * @return string
    */
	public function getHTTPStatusText()
	{
		return $this->_httpStatusText;
	}
	
   /**
    * Return HTTP status messages associated with this exception.
    * Suitable for use in HTTP header.
    *
    * @return string
    */
	public function getHTTPStatus()
	{
		return $this->_httpStatusCode . ' ' . $this->_httpStatusText;
	}
}


class SparkHTTPException_BadRequest extends SparkHTTPException
{
	public function __construct($message = NULL, $vars = NULL)
	{
		parent::__construct(400, 'Bad Request', $message, $vars);
	}
}

class SparkHTTPException_Unauthorized extends SparkHTTPException
{
	public function __construct($message = NULL, $vars = NULL)
	{
		parent::__construct(401, 'Unauthorized', $message, $vars);
	}
}

class SparkHTTPException_Forbidden extends SparkHTTPException
{
	public function __construct($message = NULL, $vars = NULL)
	{
		isset($message) || $message = 'Permission denied.';
		parent::__construct(403, 'Forbidden', $message, $vars);
	}
}

class SparkHTTPException_NotFound extends SparkHTTPException
{
	public function __construct($message = NULL, $vars = NULL)
	{
		isset($message) || $message = 'The page you requested was not found.';
		parent::__construct(404, 'Not Found', $message, $vars);
	}
}

class SparkHTTPException_MethodNotAllowed extends SparkHTTPException
{
	public function __construct($message = NULL, $vars = NULL)
	{
		parent::__construct(405, 'Method Not Allowed', $message, $vars);
	}
}

class SparkHTTPException_Conflict extends SparkHTTPException
{
	public function __construct($message = NULL, $vars = NULL)
	{
		parent::__construct(409, 'Conflict', $message, $vars);
	}
}

class SparkHTTPException_Gone extends SparkHTTPException
{
	public function __construct($message = NULL, $vars = NULL)
	{
		parent::__construct(410, 'Gone', $message, $vars);
	}
}

class SparkHTTPException_LengthRequired extends SparkHTTPException
{
	public function __construct($message = NULL, $vars = NULL)
	{
		parent::__construct(411, 'Length Required', $message, $vars);
	}
}

class SparkHTTPException_RequestEntityTooLarge extends SparkHTTPException
{
	public function __construct($message = NULL, $vars = NULL)
	{
		parent::__construct(413, 'Request Entity Too Large', $message, $vars);
	}
}

class SparkHTTPException_RequestURITooLong extends SparkHTTPException
{
	public function __construct($message = NULL, $vars = NULL)
	{
		parent::__construct(414, 'Request-URI Too Long', $message, $vars);
	}
}

class SparkHTTPException_UnsupportedMediaType extends SparkHTTPException
{
	public function __construct($message = NULL, $vars = NULL)
	{
		parent::__construct(415, 'Unsupported Media Type', $message, $vars);
	}
}

class SparkHTTPException_InternalServerError extends SparkHTTPException
{
	public function __construct($message = NULL, $vars = NULL)
	{
		isset($message) || $message = 'Your request could not be processed.';
		parent::__construct(500, 'Internal Server Error', $message, $vars);
	}
}

class SparkHTTPException_NotImplemented extends SparkHTTPException
{
	public function __construct($message = NULL, $vars = NULL)
	{
		parent::__construct(501, 'Not Implemented', $message, $vars);
	}
}

class SparkHTTPException_ServiceUnavailable extends SparkHTTPException
{
	public function __construct($message = NULL, $vars = NULL)
	{
		parent::__construct(503, 'Service Unavailable', $message, $vars);
	}
}


/**
 * The SparkPHPException class is used to automatically convert PHP errors to exceptions.
 * This is one of the few *final* core classes in Spark/Plug (not extensible by plugs).
 *
 * @package Spark/Plug
 */
// -----------------------------------------------------------------------------

final class SparkPHPException
{
   /**
    * Convert PHP error to equivalent exception.
    *
    * @param int $code
    * @param string $message
    * @param string $file
    * @param int $line
    * @throw ErrorException
    */
	public static function errorHandler($code, $message, $file, $line)
	{
		throw new ErrorException($message, 0, $code, $file, $line);
	}
}


/**
 * The SparkUtil class contains some utility functions commonly needed by web apps.
 * This is one of the few *final* core classes in Spark/Plug (not extensible by plugs).
 *
 * @package Spark/Plug
 */
// -----------------------------------------------------------------------------

final class SparkUtil
{
 	const kRequestMethod_HEAD = 1;
 	const kRequestMethod_GET = 2;
 	const kRequestMethod_POST = 3;
 	const kRequestMethod_PUT = 4;
 	const kRequestMethod_DELETE = 5;
 	const kRequestMethod_OPTIONS = 6;

	private static $_http_methods = array
	(
		'head' => self::kRequestMethod_HEAD,
		'get' => self::kRequestMethod_GET,
		'post' => self::kRequestMethod_POST,
		'put' => self::kRequestMethod_PUT,
		'delete' => self::kRequestMethod_DELETE,
		'options' => self::kRequestMethod_OPTIONS,
	);
	
  /**
    * Return document root.
    *
    * @return string
    */
	final public static function doc_root()
	{
		return $_SERVER['DOCUMENT_ROOT'];
	}

   /**
    * Return whether this page was accessed securely.
    *
    * @return boolean
    */
	final public static function is_https()
	{
		return (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on');
	}

   /**
    * Return scheme string.
    *
    * @return string
    */
	final public static function scheme()
	{
		return self::is_https() ? 'https://' : 'http://';
	}

   /**
    * Return host string.
    *
    * @return string
    */
	final public static function host()
	{
		return $_SERVER['HTTP_HOST'];
	}

   /**
    * Return server name string.
    *
    * @return string
    */
	final public static function server_name()
	{
		return $_SERVER['SERVER_NAME'];
	}

   /**
    * Return server ip address string.
    *
    * @return string
    */
	final public static function server_addr()
	{
		return $_SERVER['SERVER_ADDR'];
	}

   /**
    * Return script name.
    *
    * @return string
    */
	final public static function script_name()
	{
		return $_SERVER['SCRIPT_NAME'];
	}

   /**
    * Return original request URI.
    *
    * @return string
    */
	final public static function request_uri()
	{
		return $_SERVER['REQUEST_URI'];
	}

   /**
    * Return original request URL.
    *
    * @return string
    */
	final public static function self_url()
	{
		return self::scheme() . self::host() . self::request_uri();
	}

   /**
    * Return original request method.
    *
    * @return string
    */
	final public static function request_method()
	{
		return !empty($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : '';
	}

   /**
    * Return original query string.
    *
    * @return string
    */
	final public static function query_string()
	{
		return !empty($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '';
	}

   /**
    * Return user's browser agent string.
    *
    * @return string
    */
	final public static function user_agent()
	{
		return isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
	}

   /**
    * Return user's IP address.
    *
    * @return string
    */
	final public static function remote_ip()
	{
		return $_SERVER['REMOTE_ADDR'];
	}
	
   /**
    * Return referring url.
    *
    * @return string
    */
	final public static function referrer_url()
	{
		return isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
	}
	
   /**
    * Return referring url.
    *
    * @return string
    */
	final public static function get_http_method($method = NULL)
	{
		if (!$method)
		{
			$method = self::request_method();
		}

		if ($method = @self::$_http_methods[strtolower($method)])
		{
			return $method;
		}
		
		return NULL;
	}
	
   /**
    * Check if two IP addresses match or are similar.
    *
    * @param string $ip1
    * @param string $ip2
    * @return boolean
    */
	final public static function match_ip($ip1, $ip2, $matchOctets = 4)
	{
		// full IP match?
	
		if ($ip1 == $ip2)
		{
			return true;
		}
		
		// entire IP address did not match (probably a pesky proxy server)
		// break the address into octets
		
		$octets1 = explode('.' , $ip1, 4);
		$octets2 = explode('.' , $ip2, 4);
	
		for ($match = 0; $match < $matchOctets; ++$match)
		{
			if ($octets1[$match] != $octets2[$match])
			{
				return false;
			}
		}
		
		return true;
	}

   /**
    * Check if URL is valid.
    *
    * httpurl		= https?://{hostport}(/{hpath}(\?{search})?)?
    * hostport		= host(:{port})?
    * host			= {hostname}|{hostnumber}
    * hostname		= ({domainlabel}\.)*{toplabel}
    * domainlabel	= {alphadigit}(({alphadigit}|\-)*{alphadigit})?
    * toplabel		= {alpha}(({alphadigit}|\-)*{alphadigit})?
    * hostnumber	= {digits}\.{digits}\.{digits}\.{digits}
    * port			= {digits}
    * hpath			= {hsegment}(/{hsegment})*
    * hsegment		= ({uchar}|[;:@&=])*
    * search		= ({uchar}|[;:@&=])*
    * uchar			= {unreserved}|{escape}
    * unreserved	= {alphadigit}|{safe}|{extra}
    * escape		= %{hex}{hex}
    * safe			= [\$\-\_\.\+]
    * extra			= [\!\*\'\(\)\,]
    * hex			= [0-9a-f]
    * alpha			= [a-z]
    * digit			= [0-9]
    * alphadigit	= [a-z0-9]
    *
    * hsegment		= (?:[a-z0-9\$\-\_\.\+\!\*\'\(\)\,\;\:\@\&\=\~]|(?:%[0-9a-f]{2}))*
    *
    * return preg_match('#^https?://(?:(?:(?:[a-z0-9](?:[a-z0-9\-]*[a-z0-9])?\.)*[a-z](?:[a-z0-9\-]*[a-z0-9])?)|(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3}))(?::[0-9]{1,5})?(?:/(?:[a-z0-9\$\-\_\.\+\!\*\\\'\(\)\,\;\:\@\&\=\~]|(?:%[0-9a-f]{2}))*(?:/(?:[a-z0-9\$\-\_\.\+\!\*\\\'\(\)\,\;\:\@\&\=\~]|(?:%[0-9a-f]{2}))*)*(?:\?(?:[a-z0-9\$\-\_\.\+\!\*\\\'\(\)\,\;\:\@\&\=\~]|(?:%[0-9a-f]{2}))*)?)?$#i', $item);
    *		
    *
    * @param string $url
    * @return boolean
    */
	final public static function valid_url($url, $requireScheme = true)
	{
		return preg_match('#^(?:https?://)' . ($requireScheme ? '' : '?') . '(?:(?:(?:[a-z0-9](?:[a-z0-9\-]*[a-z0-9])?\.)*[a-z](?:[a-z0-9\-]*[a-z0-9])?)|(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3}))(?::[0-9]{1,5})?(?:/(?:[a-z0-9\$\-\_\.\+\!\*\\\'\(\)\,\;\:\@\&\=\~]|(?:%[0-9a-f]{2}))*)*(?:\?(?:[a-z0-9\$\-\_\.\+\!\*\\\'\(\)\,\;\:\@\&\=\~]|(?:%[0-9a-f]{2}))*)?$#i', $url) ? true : false;
	}

   /**
    * Check if URL path is valid.
    *
    * @param string $url
    * @return boolean
    */
	final public static function valid_url_path($url)
	{
		return preg_match('#^(?:/(?:[a-z0-9\$\-\_\.\+\!\*\\\'\(\)\,\;\:\@\&\=\~]|(?:%[0-9a-f]{2}))*)*(?:\?(?:[a-z0-9\$\-\_\.\+\!\*\\\'\(\)\,\;\:\@\&\=\~]|(?:%[0-9a-f]{2}))*)?$#i', $url) ? true : false;
	}

   /**
    * Check if email address is valid.
    *
    * @param string $email
    * @return boolean
    */
	final public static function valid_email($email)
	{
		return preg_match('/^([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}$/i', $email) ? true : false;
	}

   /**
    * Extract host from url.
    *
    * @param string $url
    * @return string
    */
	final public static function extract_host_from_url($url)
	{
		return preg_replace('#^(https?://[^/]+)(.*)$#', '\1', $url);
	}

   /**
    * Create a nice big random string.
    *
    * @return string
    */
	final public static function make_nonce($length = 32)
	{
		$nonce = '';
		do
		{
			$nonce .= sha1(uniqid(mt_rand(), true));
		} while (strlen($nonce) < $length);
		return substr($nonce, 0, $length);
	}

   /**
    * Create a version 4 random UUID.
    *
    * @return string
    */
	final public static function make_uuid()
	{
		return sprintf
		(
			'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
			mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff),
			mt_rand(0, 0x0fff) | 0x4000,
			mt_rand(0, 0x3fff) | 0x8000,
			mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
		);
	}

   /**
    * Encode a UUID into smaller ASCII string.
    *
    * @param string $uuid
    * @return string
    */

	final public static function encode_uuid($uuid)
	{
		return str_replace(array('/','+'), array('_','-'), substr(base64_encode(pack('H*', str_replace('-', '', $uuid))), 0, 22));
	}
	
   /**
    * Decode an encoded UUID.
    *
    * @param string $uuid
    * @return string
    */

	final public static function decode_uuid($uuid)
	{
		$decoded = bin2hex(base64_decode(str_replace(array('_','-'), array('/','+'), $uuid) . '=='));
		return substr($decoded, 0, 8) . '-' . substr($decoded, 8, 4) . '-' . substr($decoded, 12, 4) . '-' . substr($decoded, 16, 4) . '-' . substr($decoded, 20);
	}
	
   /**
    * Truncate a string.
    *
    * @param string $text Text to truncate
    * @param bool $maxChars truncate if longer than this
    * @param bool $encoding text encoding to use
    * @return string
    */
	final public static function truncate($text, $maxChars = 50, $encoding = NULL)
	{
		if (function_exists('mb_strimwidth'))
		{
			if ($encoding)
			{
				return mb_strimwidth($text, 0, $maxChars, '…', $encoding);
			}
			else
			{
				return mb_strimwidth($text, 0, $maxChars, '…');
			}
		}
		else
		{
			return ($maxChars >= strlen($text)) ? $text : substr($text, 0, $maxChars-1) . '…';
		}
	}

}


/**
 * The SparkInflector class is used to convert strings to/from under_score and CamelCase.
 * This class may be used by core classes, application classes and plugs.
 * This is one of the few *final* core classes in Spark/Plug (not extensible by plugs).
 *
 * @package Spark/Plug
 */
// -----------------------------------------------------------------------------

final class SparkInflector
{
   /**
    * Convert an indentifier to readable text ("example_identifier" -> "Example Identifier").
    *
    * @param string $s
    * @return string
    */
	final public static function humanize($s)
	{
		return ucfirst(str_replace('_', ' ', $s));
	}

   /**
    * Convert readable text to identifier ("Example Identifier" -> "example_identifier").
    *
    * @param string $s
    * @return string
    */
	final public static function dehumanize($s)
	{
		return strtolower(str_replace(' ', '_', $s));
	}

   /**
    * Convert an indentifier to CamelCase ("example_identifier" -> "ExampleIdentifier").
    *
    * @param string $s
    * @return string
    */
	final public static function camelize($s)
	{
		return str_replace(' ', '', ucwords(str_replace('_', ' ', $s)));
	}

   /**
    * Convert an indentifier from CamelCase to underscores ("ExampleIdentifier" -> "example_identifier").
    *
    * @param string $s
    * @return string
    */
	final public static function decamelize($s)
	{
		return trim(strtolower(preg_replace(array('/([A-Z])([a-z])/', '/([a-z])([A-Z])/'), array('_$1$2', '$1_$2'), $s)), '_');
	}
}


/**
 * The SparkConfig class is used to store and retrieve configuration data.
 * This class may be used by core classes, application classes and plugs.
 * This is one of the few *final* core classes in Spark/Plug (not extensible by plugs).
 *
 * @package Spark/Plug
 */
// -----------------------------------------------------------------------------

final class SparkConfig
{
	private $_config;		// key-value dictionary of configuration settings
	
   /**
    * Construct a new Config object.
    *
    * @param array $config: key-value dictionary of configuration values
    */
	public function __construct(&$config)
	{
		$this->_config =& $config;
	}

   /**
    * Retrieve a previously stored configuration item.
    * If the item is not found, return the provided default value (if any).
    *
    * @param string $key Name of configuration parameter to retrieve
    * @param string $default Default value to return if configuration parameter not found
    * @return any Value of configuration parameter if found, or default if not found
    */
	final public function get($key, $default = NULL)
	{
		return self::paramGet($this->_config, $key, $default);
	}

   /**
    * Store a new configuration item.
    *
    * @param string $key Name of configuration parameter to store
    * @param string $val Value to store
    */
	final public function set($key, $val)
	{
		$this->_config[$key] = $val;
	}

   /**
    * Retrieve a value from a parameter array.
    * If the item is not found, return the provided default value (if any).
    *
    * @param string $key Name of parameter to retrieve
    * @param string $default Default value to return if parameter not found
    * @return any Value of parameter if found, or default if not found
    */
	final static public function paramGet($arr, $key, $default = NULL)
	{
		return isset($arr[$key]) ? $arr[$key] : $default;
	}

   /**
    * Retrieve a (normalized) PHP configuration setting.
    *
    * @param string $key Name of configuration parameter to retrieve
    * @param string $default Default value to return if configuration parameter not found
    * @return string Value of configuration parameter if found, or default if not found
    */
	final static public function php_ini_get($key, $default = NULL)
	{
		switch (strtolower($val = ini_get($key)))
		{
			case '':
				return $default;
			
			case '0':
			case 'off':
			case 'false':
			case 'no':
			case 'none':
				return false;
			
			case '1':
			case 'on':
			case 'true':
			case 'yes':
				return true;
			
			default:
				return $val;
		}
	}
}


/**
 * The SparkObserver class is used to register observable events and their observers.
 * Once events and observers have been registered, events may be fired and all observers
 * registered for those events will be notified.
 * This class may be used by core classes, application classes and plugs.
 * This is one of the few *final* core classes in Spark/Plug (not extensible by plugs).
 *
 * @package Spark/Plug
 */
// -----------------------------------------------------------------------------

final class SparkObserver
{
	private $_events;		// list of observable events
	
   /**
    * Construct a new SparkObserver object.
    *
    */
	public function __construct()
	{
		$this->_events = array();
	}

   /**
    * Register an observer for one or more events.
    *
    * An observer can be one of:
    * 
    *   a function name
    *   a lambda function
    *   an array: (class name, static method name)
    *   an array: (object, method name) 
    *
    * One or more events can be registered for each observer.
    * Events can be specified as either an array of event names or as a 
    * comma-delimited string containing one or more event names.
    *
    * @param string $observer Called when registered events fires
    * @param string $events Event (or events) to observe
    */
	final public function observe($observer, $events)
	{
		if (!is_array($events))
		{
			$events = explode(',', str_replace(' ', '', $events));
		}
		
		// add the observer to the event list for each event it is observing
		
		foreach($events as $event)
		{
			$this->_events[$event][] = $observer;
		}
	}

   /**
    * Notify observers that an event has fired.
    * Observers will be passed the event name plus any additional args passed to
    * this method.
    *
    * @param mixed $events Event (or events) to fire
    * @param mixed $params1...$paramN variable argument list to pass to observer
    */
	final public function notify($events)
	{
		if (!is_array($events))
		{
			$events = explode(',', str_replace(' ', '', $events));
		}
		
		// A "composite event" is of the form "general:more_specific:most_specific",
		// a series of component events separated by colons in which each subsequent
		// component is more specific than the preceding component. For composite
		// events, we send multiple notifications, so listeners may listen on on as
		// specific a component as desired. For example, given composite event:
		// 	"site_changed:content:page_saved:edited"
		// we would send the event notification to listeners of the folowing four events:
		//		"site_changed"
		//		"site_changed:content"
		//		"site_changed:content:page_saved"
		// 	"site_changed:content:page_saved:edited"
		//
		// Note that the specific event (in this case, "site_changed:content:page_saved:edited")
		// is always passed as the first argument to the listener's callback function.
		
		foreach ($events as $event)
		{
			$first = true;
			foreach (explode(':', $event) as $component)
			{
				if (!empty($component))
				{
					if ($first)
					{
						$notification = $component;
						$first = false;
					}
					else
					{
						$notification .= ':' . $component;
					}
					if (isset($this->_events[$notification]))
					{
						foreach ($this->_events[$notification] as $observer)
						{
							$args = func_get_args();
							$args[0] = $event;
							call_user_func_array($observer, $args);
						}
					}
				}
			}
		}
	}
}


/**
 * The Spark class is Spark/Plug's object factory. It loads plugs and constructs
 * object hierarchies on the fly when instantiating a new object.
 * This class may be used by core classes, application classes and plugs.
 * This is one of the few *final* core classes in Spark/Plug (not extensible by plugs).
 *
 * @package Spark/Plug
 */
// -----------------------------------------------------------------------------

final class Spark
{
	private $_observer;		// global observer
	private $_plugList;		// list if installed plugs indexed by class name
	private $_plugMap;		// maps class names to list of plugs that implement/extend that class name
	private $_plugCacheDir;	// directory containing cached plugs
	private $_plugDirStack;	// directories containing plugs

	// --------------------------------------------------------------------------

   /**
    * Construct a new Spark object, bootstrap the application, and "activate" plugs.
    * Only plugs that extend (derive directly from) the SparkPlug class need be specified
    * at this time. Additional plugs may be activated at any time by invoking the findPlugs()
    * method of this class.
    *
    * @param array $classes Array of names of plugs to "activate" (make available to the running application)
    * @param string|array $searchPaths Optional path(s) to search for plugs
    * @param string $cacheDir Optional path to plug cache directory
    */
	final public function __construct($classes = array(), $searchPaths = NULL, $cacheDir = NULL)
	{
		$this->_observer = new SparkObserver();
		$this->_plugList = array();
		$this->_plugMap = array();
		$this->_plugCacheDir = NULL;

		$spDir = dirname(__FILE__);

		if (!empty($cacheDir))
		{
			$this->_plugCacheDir = $cacheDir;
			if ($this->_plugCacheDir[0] !== '/')
			{
				$this->_plugCacheDir = "{$spDir}/{$this->_plugCacheDir}";
			}
			$this->_observer->observe(array($this, 'flushPlugCache'), 'Spark:cache:request_flush');
		}
		
		if (empty($searchPaths))
		{
			$searchPaths = array(NULL);
		}
		foreach ($searchPaths as $dir)
		{
			if (empty($dir))
			{
				$this->_plugDirStack[] = "{$spDir}/plugs";
			}
			else
			{
				if ($dir[0] !== '/')
				{
					$dir = "{$spDir}/{$dir}";
				}
				$this->_plugDirStack[] = $dir;
			}
		}

		$this->findPlugs($classes);

		// load the SparkPlug class and all its plugs

		$this->loadClass('SparkPlug');
	}

	// --------------------------------------------------------------------------

   /**
    * Return the global observer object
    *
    * @return object Observer object
    */
	final public function observer()
	{
		return $this->_observer;
	}

	// --------------------------------------------------------------------------

   /**
    * Initialize the Spark object and load plugs that extend core classes.
    * This method is invoked at the bottom of this file.
    *
    */
	final public function init()
	{
		// load all the core classes and their respective plugs
		
		$this->loadClass('SparkModel');
		$this->loadClass('SparkView');
		$this->loadClass('SparkController');
		$this->loadClass('SparkApplication');
	}

	// --------------------------------------------------------------------------

	public function flushPlugCache()
	{
		if (!empty($this->_plugCacheDir))
		{
			$iter = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->_plugCacheDir), RecursiveIteratorIterator::CHILD_FIRST);
			foreach ($iter as $file)
			{
				$filePath = $file->getPathname();
				if ($file->isDir())
				{
					rmdir($filePath);
				}
				else
				{
					unlink($filePath);
				}
			}
		}
	}

	// --------------------------------------------------------------------------

   /**
    * Call this method to make additional plugs available to be loaded.
    * Subsequent invocations of manufacture() will consider these additional plugs when
    * determining which plugs to load. If $searchPaths parameter is provided, it will
    * be used instead of the main plug directory for the scope of this invocation only.
    * If $callback is provided, it will be called for each plug and passed the plug as a
    * parameter.
    *
    * @param array $classes Array of plug names to make available
    * @param string|array $searchPaths Optional path(s) to search for plugs
    * @param string $callback Callback (optional)
    */
	final public function findPlugs($classes, $searchPaths = NULL, $callback = NULL)
	{
		if (!empty($classes))
		{
			if (empty($searchPaths))
			{
				$searchPaths = $this->_plugDirStack;
			}
			elseif (!is_array($searchPaths))
			{
				$searchPaths = array($searchPaths);
			}
			
			foreach ($classes as $class)
			{
				// allow nested plug directories
				
				if ($subPath = (strpos($class, '/') === false) ? '' : '/'.dirName($class))
				{
					$class = basename($class);
				}

				foreach ($searchPaths as $plugDir)
				{
					$dirName = $plugDir . $subPath . '/' . SparkInflector::decamelize($class);
					if (file_exists($dirName))
					{
						break;
					}
				}

				// load plug's manifest

				unset($plug);
				require($dirName . '/manifest.php');
				
				// add plug to plug map

				if (isset($plug))
				{
					$plugs = $plug['plugs'];
					unset($plug['plugs']);
					
					foreach ($plugs as $nextPlug)
					{							
						if ($callback)
						{
							$nextPlug = array_merge($plug, $nextPlug);	// make sure callback has all the plug info

							// Note: We do not use call_user_func() here because it does not allow pass-by-reference

							if (is_string($callback))
							{
								if (!$callback($nextPlug))
								{
									continue;
								}
							}
							elseif (is_array($callback))
							{
								if (!$callback[0]->$callback[1]($nextPlug))
								{
									continue;
								}
							}
						}

						$name = $nextPlug['name'];
						
						// if an explicit path is provided, use it, otherwise deduce a path from the plug's name
						
						if ($path = @$nextPlug['path'])
						{
							// explicit path can be absolute, or relative to plug's Directory
							
							if ($path[0] != '/')
							{
								$path = $dirName . '/' . $path;
							}
						}
						else
						{
							$path = $dirName . '/' . SparkInflector::decamelize($name) . '.php';
						}
						
						// plug can be one of two types:
						//
						// 1. an extension plug that augments an existing class,
						//    or
						// 2. a base plug that simply provides some new service
						
						if ($extends = @$nextPlug['extends'])
						{
							// extension plugs can declare a preferred load order to help avoid conflicts
							
							$order = isset($nextPlug['order']) ? max(1, min(100, $nextPlug['order'])) : 50;
						}
						else
						{
							$extends = $name;
							$order = 0;
						}
						$plugInfo = array('name'=>$name, 'extends'=>$extends, 'order'=>$order, 'file'=>$path);
						if (!empty($nextPlug['requires']))
						{
							if (is_string($requires = $nextPlug['requires']))
							{
								$requires = array_map('trim', explode(',',  $requires));
							}
							$plugInfo['requires'] = $requires;
						}
						if (($order === 0) && isset($nextPlug['base_class']))
						{
							$plugInfo['base_class'] = $nextPlug['base_class'];
						}
						$this->_plugList[$name] =& $plugInfo;
						$this->_plugMap[$extends][$order][] =& $plugInfo;
						unset($plugInfo);
					}
					unset($plug);
				}
			}
			
			// sort plugs according to load order
			
			foreach(array_keys($this->_plugMap) as $key)
			{
				ksort($this->_plugMap[$key]);
			}
		}
	}

	// --------------------------------------------------------------------------

   /**
    * Call this method to make an additional plug available to be loaded.
    * Subsequent invocations of manufacture() will consider this plug when
    * determining which plugs to load. Unlike findPlugs(), this method can
    * accept a callback to load the plug's code, rather than a file.
    *
    * @param array $plug:
    *		(
    *			string 'name'		=> name of plug to add,
    *			string 'requires'	=> name(s) of class(es) (if any) required by this plug,
    *			string 'extends'	=> name of class (if any) being extended by this plug,
    *			int 'order'			=> load order for this plug
    *			string 'file'		=> location of plug's code file, OR
    *			mixed 'callback'	=> callback to create the plug,
    */
	final public function addPlug($plug)
	{
		if (!empty($plug['requires']))
		{
			if (is_string($requires = $plug['requires']))
			{
				$requires = array_map('trim', explode(',',  $requires));
			}
			$plug['requires'] = $requires;
		}
		$plug['order'] = !empty($plug['extends']) ? max(1, min(100, !empty($plug['order']) ? $plug['order'] : 100)) : 0;
		if (empty($plug['extends']))
		{
			$plug['extends'] = $plug['name'];
		}
		$this->_plugList[$plug['name']] =& $plug;
		$this->_plugMap[$plug['extends']][$plug['order']][] =& $plug;
		ksort($this->_plugMap[$plug['extends']]);
	}
	
	// --------------------------------------------------------------------------

   /**
    * Call this method to retrieve information about an installed plug.
    *
    * @param string $name:		=> name of plug
    * @return array of plug info
    */
	final public function getPlug($name)
	{
		return @$this->_plugList[$name];
	}
	
	// --------------------------------------------------------------------------

   /**
    * Call this method to retrieve information about all installed plugs that extend a specified plug.
    *
    * @param string $name:		=> name of extended plug
    * @return array of plug infos
    */
	final public function getExtensions($name)
	{
		if (empty($this->_plugMap[$name]))
		{
			return NULL;
		}
		
		$extensions = array();
		foreach ($this->_plugMap[$name] as $plugList)
		{
			$extensions = array_merge($extensions, $plugList);
		}
		
		return $extensions;
	}
	
	// --------------------------------------------------------------------------

   /**
    * Dynamically build a class hierarchy and instantiate a new object.
    * This method is used to create any object that you want to be extensible.
    *
    * For example, any object derived from SparkPlug may call:
    *
    *    $myObject = $this->factory->manufacture('MyClass');
    *
    * and $myObject will be created and will include the functionality provided by
    * all plugs that extend MyClass.
    *
    * However, if instead you call:
    *
    *    $myObject = new MyClass;
    *
    * you will still create a MyClass object, but it will not include any plug functionality.
    *
    * @param string $class Name of class to instantiate
    * @return object
    */
	final public function manufacture($class)
	{
		$this->_observer->notify('Spark:manufacture:' . $class, $class);

		// load the class and all its extension plugs
	
		$class = $this->loadClass($class);

		// get parameters for constructor (remove the class name from start of args)

		$params = func_get_args();
		array_shift($params);
		
		// return call_user_func_array(array($class, '__construct'), $params);
		// can't call a constructor via call_user_func_array() because it is not a static method

		switch ($c = count($params))
		{
			case 0:
				return new $class();
			case 1:
				return new $class($params[0]);
			case 2:
				return new $class($params[0], $params[1]);
			case 3:
				return new $class($params[0], $params[1], $params[2]);
			case 4:
				return new $class($params[0], $params[1], $params[2], $params[3]);
			case 5:
				return new $class($params[0], $params[1], $params[2], $params[3], $params[4]);
			case 6:
				return new $class($params[0], $params[1], $params[2], $params[3], $params[4], $params[5]);
			case 7:
				return new $class($params[0], $params[1], $params[2], $params[3], $params[4], $params[5], $params[6]);
			case 8:
				return new $class($params[0], $params[1], $params[2], $params[3], $params[4], $params[5], $params[6], $params[7]);
			case 9:
				return new $class($params[0], $params[1], $params[2], $params[3], $params[4], $params[5], $params[6], $params[7], $params[8]);
			case 10:
				return new $class($params[0], $params[1], $params[2], $params[3], $params[4], $params[5], $params[6], $params[7], $params[8], $params[9]);
			default:
				throw new SparkException('too many arguments (' . $c . ') passed to constructor (max 10)');
		}
	}

	// --------------------------------------------------------------------------

   /**
    * Call this method to load a class and all its extension plugs.
    * Optionally cache the class to the plug cache directory.
    *
    * @param string $class Name of class to load
    * @param boolean $abstract True if this is an abstract base class
    * @return string Name of instantiated class (alternate implementation may change class name)
    */
	final public function loadClass($class, $abstract = false)
	{
		// no need to load a class more than once

		if (class_exists($class, false))
		{
			return $class;
		}
		
		// If caching is enabled, check the cache directory for this class.

		if (!empty($this->_plugCacheDir))
		{
			$cacheFile = $this->_plugCacheDir . '/' . $class . '.php';
			if ((@include $cacheFile) !== false)
			{
				return $class;
			}
		}
		
		// Extensible classes are always declared with an underscore prefix on the name,
		// which allows us to use the class as a base class for plugs, while leaving the
		// ultimate class name (without the underscore) free for dynamic creation once
		// all the plugs have been loaded.
		
		$baseClass = '_' . $class;

		// if the base class does not exist, it is a plug itself and needs to be loaded
		
		if (isset($this->_plugMap[$class]))					// any plugs want to extend this class?
		{
			foreach ($this->_plugMap[$class] as $plugs)
			{
				foreach ($plugs as $plug)
				{
					$plugName = $plug['name'];					// plug's name
					$plugOrder = $plug['order'];				// plug's load order
					$plugCode = false;							// plug's code
					
					// take care of dependencies
					
					foreach ((array)@$plug['requires'] as $dependency)
					{
						$this->loadClass($dependency);
					}

					// To allow easier integration with external class libraries, the manifest may
					// specify alternate class names for base classes.
					
					if (($plugOrder == 0) && isset($plug['base_class']))
					{
						$baseClass = $plug['base_class'];
					}
					
					// load the plug's code
					
					if ($plugFile = @$plug['file'])
					{
						$plugCode = file_get_contents($plugFile);
					}
					elseif ($callback = @$plug['callback'])
					{
						// Note: We do not use call_user_func() here because it does not allow pass-by-reference
						
						if (is_string($callback))
						{
							$plugCode = $callback($plug);
						}
						elseif (is_array($callback))
						{
							$plugCode = $callback[0]->$callback[1]($plug);
						}
					}
					
					if ($plugCode !== false)
					{
						$plugCode = preg_replace("/\s*<\?(php)?/", '', $plugCode, 1);

						// Here's the tricky bit... We dynamically rewrite the plug's class definition to build our
						// class hierarchy. (Self-modifying code - oh my!)
						// So, each plug is loaded in turn and serves as the base class for the next plug to be loaded.
						// Performance of this mechanism actually turns out to be quite respectable. The major downside
						// is that opcode cachers will not be able to cache the bytecode of plugs loaded in this manner,
						// (which is generally true of eval'd code). However, this drawback may be mitigated by enabling
						// plug caching, which writes the entire class inheritance chain to a single class file.
						
						if ($plugOrder != 0)		// not a base class?
						{
							$firstClass = isset($plug['first_class']) ? $plug['first_class'] : $plugName;

							$plugCode = preg_replace("/class\s+{$firstClass}\s+extends\s+(?:{$class}|{$baseClass})/", "class {$firstClass} extends {$baseClass}", $plugCode, 1, $count);
	
							if ($count !== 1)		// something fishy here, best abort
							{
								throw new SparkException('plug err: Could not load plug "' . ($plugFile ? $plugFile : $plugName) . '"');
							}
						}
						
						// create cache file if caching enabled
						
						if (!isset($cacheFileHandle) && !empty($this->_plugCacheDir))
						{
							if (($cacheFileHandle = @fopen($cacheFile, 'w')) !== false)
							{
								if (fwrite($cacheFileHandle, "<?php\n") === false)
								{
									throw new SparkException('plug err: Could not write plug cache file "' . $cacheFile . '"');
								}
							}
						}
						
						if (!empty($cacheFileHandle))
						{
							if (fwrite($cacheFileHandle, $plugCode) === false)
							{
								throw new SparkException('plug err: Could not write plug cache file "' . $cacheFile . '"');
							}
						}
						elseif ($this->loadPlug($plugCode) === false)
						{
							throw new SparkException('plug err: Could not load plug "' . ($plugFile ? $plugFile : $plugName) . '"');
						}
						
						// plug successfully loaded and will serve as the base class for the next to load (if any)
						
						if ($plugOrder != 0)		// not a base class?
						{
							$baseClass = $plugName;
						}
					}
				}
			}
		}
		
		// Finally, dynamically generate the requested class as a simple wrapper class.
		// This makes the class name a legitimate object in the PHP namespace so it can be referred to by
		// other functions, etc.
				
		$plugCode = "\nif (!class_exists('{$class}', false)) { " . ($abstract ? 'abstract ' : '') . "class {$class} extends {$baseClass} {} }\n";

		if (!empty($cacheFileHandle))
		{
			if (fwrite($cacheFileHandle, $plugCode) === false)
			{
				throw new SparkException('plug err: Could not write plug cache file "' . $cacheFile . '"');
			}
			fclose($cacheFileHandle);
			include $cacheFile;
		}
		else
		{
			if (!class_exists($baseClass, false))
			{
				throw new SparkException('loader err: Could not load class "' . $baseClass . '"');
			}
			$this->loadPlug($plugCode);
		}

		return $class;
	}
	
	// --------------------------------------------------------------------------

   /**
    * Private method to isolate the loading of the plug's code.
    *
    * @param string $plugCode Plug code to load into PHP interpreter
    * @return mixed Result of loading the plug's code
    */
	final private function loadPlug($plugCode)
	{
		return eval($plugCode);
	}
}


/**
 * The _SparkPlug (SparkPlug) class is Spark/Plug's universal base object class. Any
 * class that wants access to the application object and/or the Spark factory object
 * should derive from this class or one of its descendants. This class is not meant to
 * be instantiated directly. When specifying this class as a base class in your class
 * definition, be sure to extend SparkPlug (not _SparkPlug). Otherwise, your derived class
 * will not inherit the capabilities of any plugs that extend this class.
 * This class may be used by core classes, application classes and plugs.
 *
 * @package Spark/Plug
 */
// -----------------------------------------------------------------------------

abstract class _SparkPlug
{
	public $app;				// convenience reference to global application object
	public $config;			// convenience reference to application configuration store
	public $factory;			// convenience reference back global object factory
	public $observer;			// convenience reference to global observer

	// --------------------------------------------------------------------------

   /**
    * Construct a new SparkPlug object. Make key application objects available to all
    * derived classes.
    */
	public function __construct()
	{
		$this->app = SparkApplication::instance();
		$this->config = $this->app->config();
		$this->factory = $this->app->factory();
		$this->observer = $this->factory->observer();
	}

   /**
    * Create a new model object. Calls through to the application's newModel() method.
    *
    * @param string $name Name of model class to instatiate
    * @return object Model object
    */
	public function newModel($name, $params = NULL)
	{
		return $this->app->newModel($name, $params);
	}

   /**
    * Create a full url to the specified (static) path. Calls through to the application's urlToStatic() method.
    *
    * @param string $uri Path to which to create full URL
    * @param bool $withHost whether to include host in URL
    * @param bool $withScheme whether to include scheme in URL
    * @return string URL
    */
	public function urlToStatic($uri, $withHost = false, $withScheme = true)
	{
		return $this->app->urlToStatic($uri, $withHost, $withScheme);
	}

   /**
    * Create a full url to the specified (dynamic) path. Calls through to the application's urlTo() method.
    *
    * @param string $uri Path to which to create full URL
    * @param bool $withHost whether to include host in URL
    * @param bool $withScheme whether to include scheme in URL
    * @return string URL
    */
	public function urlTo($uri, $withHost = false, $withScheme = true)
	{
		return $this->app->urlTo($uri, $withHost, $withScheme);
	}

   /**
    * Create a redirect to the specified (static) path. Calls through to the application's urlToStatic() method.
    *
    * @param string $uri Path to which to create full URL to redirect to
    * @param bool $secure Whether to redirect to a secure page (true), a non-secure page (false), or default (NULL)
    */
	public function redirectStatic($uri, $secure = NULL)
	{
		header('Location: ' . $this->urlToStatic($uri, true, $secure === NULL ? true : ($secure ? 'https' : 'http')));
		exit;
	}
   /**
    * Create a redirect to the specified path. Calls through to the application's urlTo() method.
    *
    * @param string $uri Path to which to create full URL to redirect to
    * @param bool $secure Whether to redirect to a secure page (true), a non-secure page (false), or default (NULL)
    */
	public function redirect($uri, $secure = NULL)
	{
		header('Location: ' . $this->urlTo($uri, true, $secure === NULL ? true : ($secure ? 'https' : 'http')));
		exit;
	}
}


// -----------------------------------------------------------------------------

/**
 * Now we have defined the core classes that allow us to create the global factory object.
 * We must instantiate the global factory object now because doing so will dynamically create
 * the SparkPlug class definition, which the following class declarations depend on.
 * The file that includes this file my declare a global array of plugs to load into the application.
 * We pass that array to the constructor.
*/

$spark = new Spark(isset($plugs) ? $plugs : NULL, isset($plug_search_paths) ? $plug_search_paths : NULL, isset($plug_cache_dir) ? $plug_cache_dir : NULL);
unset($plugs);
unset($plug_search_paths);
unset($plug_cache_dir);

// -----------------------------------------------------------------------------


/**
 * The _SparkModel (SparkModel) class is Spark/Plug's model base class.
 * While Spark/Plug is a model-view-controller framework, use of models is optional.
 * If your application uses models, they should be derived from this class or one of its
 * descendants. When specifying this class as a base class in your class definition, be sure
 * to extend SparkModel (not _SparkModel). Otherwise, your derived class will not inherit the
 * capabilities of any plugs that extend this class.
 *
 * To create a new model, invoke the newModel() method on any object derived from SparkPlug:
 *
 *    $model = $this->newModel('MyModel');
 *
 * @package Spark/Plug
 */
// -----------------------------------------------------------------------------

abstract class _SparkModel extends SparkPlug
{
   /**
    * Construct a new SparkModel object.
    * Not much to see here. Move along...
    */

	public function __construct()
	{
		parent::__construct();
	}

	//---------------------------------------------------------------------------

   /**
    * Convenience function to return the current date/time UTC in a database-friendly format.
    *
    * @return string Current date/time UTC
    */
	public static function now()
	{
		return gmdate('Y-m-d H:i:s');
	}

	//---------------------------------------------------------------------------

   /**
    * Convenience function to return an "empty" date in a database-friendly format.
    *
    * @return string Empty date
    */
	public static function never()
	{
		return '0000-00-00';
	}
}


/**
 * The _SparkView (SparkView) class is Spark/Plug's view base object class.
 * This class is not meant to be instantiated directly. When specifying this
 * class as a base class in your class definition, be sure to extend SparkView (not _SparkView).
 * Otherwise, your derived class will not inherit the capabilities of any plugs that extend this class.
 * This class will not generally be used by Spark/Plug applications directly, as Spark/Plug automatically
 * creates a single view class on initialization, which is reused for all view rendering.
 *
 * @package Spark/Plug
 */
// -----------------------------------------------------------------------------

abstract class _SparkView extends SparkPlug
{
	private $_viewDirStack;			// list of directories where view files can be found
	private $_viewStack;				// keeps track of nested view contexts
	private $_viewDefault;			// name of default view (rendered if none is explicitly specified)
	private $_viewNameExtension;	// optional view filename extension
	
   /**
    * Construct a new SparkView object.
    * Not much to see here. Move along...
    */
	public function __construct($viewDir, $viewNameExtension = '')
	{
		parent::__construct();
		$this->_viewDirStack = (array)$viewDir;
		$this->_viewStack = array();
		$this->_viewDefault = '';
		$this->_viewNameExtension = $viewNameExtension;
	}

	// --------------------------------------------------------------------------

   /**
    * Set name of default view (rendered if none specified).
    *
    * @param string $viewDefault Name of default view
    */
	final public function setDefault($viewDefault)
	{
		$this->_viewDefault = $viewDefault;
	}

	// --------------------------------------------------------------------------

   /**
    * Push a view directory onto the stack.
    *
    * @param string $dir Path to view directory
    */
	final public function pushViewDir($dir)
	{
		$this->_viewDirStack[] = $dir;
	}

	// --------------------------------------------------------------------------

   /**
    * Pop the view directory stack.
    *
    */
	final public function popViewDir()
	{
		if (count($this->_viewDirStack) > 1)
		{
			return array_pop($this->_viewDirStack);
		}
		return $this->_viewDirStack[0];
	}

	// --------------------------------------------------------------------------

   /**
    * Render a view file.
    * Accepts 0, 1, 2 or 3 parameters. Parameters may appear in any order and are as follows:
    *
    * @param string $viewList Comma-delimited array of names of views to render (first found will be rendered)
    * @param array $vars Array of variables to make available to view (extract into view as locals)
    * @param bool $returnBuffer true if rendered result should be returned instead of buffered for display
    * @return string Result of render (if $returnBuffer is true)
    */
	final public function render()
	{
		$viewList = NULL;
		$vars = array();
		$returnBuffer = false;
		
		// decode the argument list - all are optional and they can appear in any order
		
		foreach (func_get_args() as $arg)
		{
			if (is_string($arg))
			{
				$viewList = array_map('trim', explode(',', $arg));
			}
			elseif (is_array($arg))
			{
				$vars = $arg;
			}
			elseif (is_bool($arg))
			{
				$returnBuffer = $arg;
			}
		}
		
		$nested = !empty($this->_viewStack);
		
		// use the default view if no view specified
		
		if (!$nested && empty($viewList))
		{
			$viewList = array($this->_viewDefault);
		}
		
		// start buffering if necessary
		
		if ($returnBuffer || !$nested)
		{
			ob_start();
		}

		// nested views inherit the variables of the containing view(s)

		if ($nested)
		{
			$vars = array_merge(end($this->_viewStack), $vars);
		}
		
		// push our context on a stack, for use by views nested within this one
		
		$this->_viewStack[] = $vars;
		
		// load the view
		
		$foundIt = false;
		$viewNames = array_map(array('SparkInflector', 'decamelize'), $viewList);

		try
		{
			for ($iViewDir = count($this->_viewDirStack) - 1; $iViewDir >= 0; --$iViewDir)
			{
				foreach ($viewList as $iView => $view)
				{
					$viewFileName = $viewNames[$iView] . $this->_viewNameExtension . '.php';

					if (file_exists($viewPath = "{$this->_viewDirStack[$iViewDir]}/{$viewFileName}"))
					{
	
						// send pre_render notification, allowing recipient to alter the contents of the vars array
						
						$this->observer->notify('SparkView:render:before:' . $view, $view, (object)array('vars'=>&$vars));
			
						$this->loadView($viewPath, $vars);
						$foundIt = true;
	
						// send post_render notification
						
						$this->observer->notify('SparkView:render:after:' . $view, $view);
						
						break 2;
					}
				}
			}
			if (!$foundIt)
			{
				trigger_error("view file not found: {$viewFileName}" ,E_USER_ERROR);
			}
		}
		catch (Exception $e)
		{
			array_pop($this->_viewStack);
			throw $e;
		}

		array_pop($this->_viewStack);
		
		// return the rendered result if this is the outer-most view (or if caller requested it)
		
		if ($returnBuffer || !$nested)
		{
			return ob_get_clean();
		}
	}

	// --------------------------------------------------------------------------

   /**
    * Escape item for inclusion in HTML output.
    *
    * @param string $item Text to escape
    * @param boolean $encodeAll Whether to encode all characters to entity equivalents
    * @param string $charset Character set of text
    * @return string Escaped item
    */
	final public static function escape_html($item, $encodeAll = false, $charset = 'UTF-8')
	{
		return $encodeAll 
					? htmlentities($item, ENT_QUOTES, $charset)
					: htmlspecialchars($item, ENT_QUOTES, $charset);
	}

	// deprecated alias for escape_html
	
	final public static function escape($item, $encodeAll = false, $charset = 'UTF-8')
	{
		return self::escape_html($item, $encodeAll, $charset);
	}

	// --------------------------------------------------------------------------

   /**
    * Escape item for inclusion in XML output.
    *
    * @param string $item Text to escape
    * @param string $charset Character set of text
    * @return string Escaped item
    */
	final public static function escape_xml($item, $charset = 'UTF-8')
	{
		return htmlspecialchars($item, ENT_NOQUOTES, $charset);
	}

	// --------------------------------------------------------------------------

   /**
    * Escape item for inclusion in query string or form-encoded output.
    *
    * @param string $item Text to escape
    * @param string $charset Character set of text (ignored)
    * @return string Escaped item
    */
	final public static function escape_uri($item, $charset = 'UTF-8')
	{
		return urlencode($item);
	}

	// --------------------------------------------------------------------------

   /**
    * Load a view file.
    *
    * Note the funky use of func_get_args() to retrieve the filename. This is due to
    * the fact that the extract operation can potentially overwrite the $file param
    * with a local var named 'file'.
    *
    * @param string $file Path to view file
    * @param array $vars Array of variables to make available to view (extract into view as locals)
    */

	final private static function array_first($array) { return $array[0]; }
	final private function loadView($file, $vars)
	{
		extract($vars, EXTR_OVERWRITE);
		unset($vars);
		require(self::array_first(func_get_args()));
	}
}


/**
 * The _SparkController (SparkController) class is Spark/Plug's controller base class.
 * Your application's controllers should be derived from this class or one of its
 * descendants. When specifying this class as a base class in your class definition, be sure
 * to extend SparkController (not _SparkController). Otherwise, your derived class will not
 * inherit the capabilities of any plugs that extend this class.
 *
 * Your application will not generally create controller objecct directly, as Spark/Plug
 * automatically instantiates a controller object based on the current URL.
 *
 * @package Spark/Plug
 */
// -----------------------------------------------------------------------------

abstract class _SparkController extends SparkPlug
{
   /**
    * Construct a new SparkController object.
    * Not much to see here. Move along...
    */
	public function __construct()
	{
		parent::__construct();
	}
	
   /**
    * Default implementations for all RESTful actions.
    *
    * @param array $params Array of parameters for controller
    */
	public function action_index($params)
	{
		throw new SparkHTTPException_MethodNotAllowed();
	}
	public function action_create($params)
	{
		throw new SparkHTTPException_MethodNotAllowed();
	}
	public function action_clear($params)
	{
		throw new SparkHTTPException_MethodNotAllowed();
	}
	public function action_show($params)
	{
		throw new SparkHTTPException_MethodNotAllowed();
	}
	public function action_append($params)
	{
		throw new SparkHTTPException_MethodNotAllowed();
	}
	public function action_update($params)
	{
		throw new SparkHTTPException_MethodNotAllowed();
	}
	public function action_destroy($params)
	{
		throw new SparkHTTPException_MethodNotAllowed();
	}
	public function action_options($params)
	{
		throw new SparkHTTPException_MethodNotAllowed();
	}
	
   /**
    * Default implementation of _before_dispatch().
    *
    * @param string $method Name of method about to be invoked
    * @param array $params Array of parameters for controller
    * @return boolean true -> proceed with method call, false -> abort method calll
    */
	public function _before_dispatch($method, $params)
	{
		return true;
	}
	
   /**
    * Convenience method to render a view file. Calls through to app's render method.
    * Accepts 0, 1, 2 or 3 parameters. Parameters may appear in any order and are as follows:
    *
    * @param string $viewList Comma-delimited array of names of views to render (first found will be rendered)
    * @param array $vars Array of variables to make available to view (extract into view as locals)
    * @param bool $returnBuffer true if rendered result should be returned instead of buffered for display
    * @return string Result of render (if $returnBuffer is true)
    */
	protected function render()
	{
		$args = func_get_args();
		return call_user_func_array(array($this->app, 'render'), $args);
	}

   /**
    * Convenience method to encode response data according to its content type.
    * Calls through to app's encodeResponseData method.
    *
    * @param string $data Response data string
    * @param string $contentType HTTP Content-Type
    * @return string Returns ancoded data as a string
    */
	public function encodeResponseData($data, $contentType)
	{
		return $this->app->encodeResponseData($data, $contentType);
	}

   /**
    * Convenience method to send output to the browser. Calls through to app's display method.
    *
    * @param string $output Display-ready content to send to browser
    * @param string $contentType Content type to send to browser
    * @param string $status HTTP status code to set in header (optional)
    * @return string Output is also returned to caller
    */
	protected function display($output, $contentType = 'text/html', $status = NULL, $headers = NULL)
	{
		return $this->app->display($output, $contentType, $status, $headers);
	}

   /**
    * Convenience method to send download to the browser. Calls through to app's download method.
    *
    * @param string $output Content to download to browser
    * @param string $contentType Content type to send to browser
    * @param string $fileName Name of file we're sending to browser
    * @param int $fileSize Size of file we're sending to browser
    */
	protected function download($output, $contentType, $fileName, $fileSize)
	{
		return $this->app->download($output, $contentType, $fileName, $fileSize);
	}

   /**
    * Convenience method to drop the first parameter from a parameter list.
    * Useful when performing sub-dispatch based on initial parameter(s)
    *
    * @param array &$params List of parameters from which to drop
    * @param int $count Number of parameters to drop
    * @return array &$params is also returned to caller
    */
	protected function &dropParam(&$params, $count = 1)
	{
		while ($count-- && $params['count'])
		{
			array_shift($params);
			array_shift($params['segments']);
			--$params['count'];
		}
		return $params;
	}
}


/**
 * The _SparkApplication (SparkApplication) class is Spark/Plug's application base class.
 * Your custom application should be derived from this class or one of its
 * descendants. When specifying this class as a base class in your class definition, be sure
 * to extend SparkApplication (not _SparkApplication). Otherwise, your derived class will not
 * inherit the capabilities of any plugs that extend this class.
 *
 * If your derived class has a custom constructor, be sure to accept the same initial parameters
 * as SparkApplication::__construct() and pass those parameters to the parent:
 *
		parent::__construct($spark, $config);
 *
 * @package Spark/Plug
 */
// -----------------------------------------------------------------------------

abstract class _SparkApplication extends SparkPlug
{
	private static $_instance;				// application singleton

	private $_factory;						// object factory
	private $_config;							// application config object
	private $_supportedTypes;				// array of media types we support (type => extension)
	private $_iSupportedTypes;				// flipped media types array (extension => type)
	private $_RESTful;						// whether to use REST routing/dispatching
	private $_emulateHTTPMethods;			// whether to emulate PUT/DELETE/etc. via overload POST
	private $_routeExtra;					// whether to allow/preserve extra routing information at end of URL
	private $_appHost;						// application host name
	private $_appHostSecure;				// application host name for secure pages
	private $_spDir;							// directory holding Spark/Plug framework code
	private $_appDir;							// directory holding application's models, views, controllers, etc.
	private $_controllerNameExtension;	// optional controller filename extension
	private $_modelDirStack;				// list of directories where model files can be found
	private $_controllerDirStack;			// list of directories where controller files can be found
	private $_view;							// global view instance
	private $_charset;						// character set for content
	private $_requestMethod;				// http request method
	private $_scriptName;					// our script name
	private $_scriptFile;					// our script file
	private $_urlBase;						// URL prefix minus the script file (eg. without index.php)
	private $_urlPrefix;						// auto-detected application URL prefix
	private $_output;							// display buffer
	
	// --------------------------------------------------------------------------

   /**
    * Access method to return application singleton object.
    * Called by static methods that need access to the application object.
    *
    * @return object Application singleton object
    */
	final public static function instance()
	{
		return self::$_instance;
	}

	// --------------------------------------------------------------------------

   /**
    * Set HTTP status header.
    *
    * @param string $output Display-ready content to send to browser
    * @param string $contentType Content type to send to browser
    * @return string Output is also returned to caller
    */
	final public static function setStatus($status = '200 OK')
	{
		header('HTTP/1.1 ' . $status);
	}

	// --------------------------------------------------------------------------

   /**
    * Utility method to kill any automatic global variables (in case register_globals is on).
    */
	final private static function killGlobals()
	{
		$protected = array
		(
			'_SERVER'=>1, '_GET'=>1, '_POST'=>1, '_FILES'=>1, '_REQUEST'=>1, '_SESSION'=>1, '_ENV'=>1, 'GLOBALS'=>1, 'HTTP_RAW_POST_DATA'=>1,
		);
		
		foreach (array($_GET, $_POST, $_COOKIE, $_SERVER, $_FILES, $_ENV, (isset($_SESSION) && is_array($_SESSION)) ? $_SESSION : array()) as $global)
		{
			if (!is_array($global))
			{
				if (!isset($protected[$global]))
				{
					unset($GLOBALS[$global]);
				}
			}
			else
			{
				foreach ($global as $key => $val)
				{
					if (!isset($protected[$key]))
					{
						unset($GLOBALS[$key]);
					}
					if (is_array($val))
					{
						foreach($val as $k => $v)
						{
							if (!isset($protected[$k]))
							{
								unset($GLOBALS[$k]);
							}
						}
					}
				}
			}
		}
	}

	// --------------------------------------------------------------------------

   /**
    * Construct a new SparkApplication object.
    *
    * @param object $spark Global object factory
    * @param array $config Configuration data
    * @return object New application object
    */
	public function __construct($spark, $config = array())
	{
		if (self::$_instance)
		{
			throw new SparkException('only one instance of SparkApplication allowed');
		}
		
		self::$_instance = $this;		// assign the singleton
		
		// we need to constuct our parent as soon as possible - but not until we have assigned a few critical properties
		
		$this->_factory = $spark;
		$this->_config = new SparkConfig($config);

		$this->_supportedTypes = !empty($config['supported_types']) ? $config['supported_types'] : array('html'=>'text/html');
		$this->_iSupportedTypes = array_flip($this->_supportedTypes);
		$this->_RESTful = !empty($config['RESTful']);
		$this->_emulateHTTPMethods = isset($config['emulate_http_methods']) ? $config['emulate_http_methods'] : $this->_RESTful;
		$this->_routeExtra = isset($config['route_extra']) ? $config['route_extra'] : true;

		($this->_appHost = @$config['app_host']) || ($this->_appHost = $config['app_host'] = SparkUtil::host()) || ($this->_appHost = $config['app_host'] = SparkUtil::server_name());
		($this->_appHostSecure = @$config['app_host_secure']) || ($this->_appHostSecure = $this->_appHost);

		($this->_spDir = @$config['sparkplug_dir']) || ($this->_spDir = $config['sparkplug_dir'] = dirname(__FILE__));
		($this->_appDir = @$config['app_dir']) || ($this->_appDir = $config['app_dir'] = $this->_spDir);

		$this->_modelNameExtension = empty($config['model_name_extension']) ? '' : "_{$config['model_name_extension']}";
		$this->_controllerNameExtension = empty($config['controller_name_extension']) ? '' : "_{$config['controller_name_extension']}";

		$this->_modelDirStack[] = "{$this->_appDir}/models";
		$this->_controllerDirStack[] = "{$this->_appDir}/controllers";

		$this->_charset = $this->_config->get('charset', 'UTF-8');
		
		// get request method
		
		$this->_requestMethod = SparkUtil::get_http_method();

		// get script name, script file, url base

		if (($this->_scriptName = SparkUtil::script_name()) === '')
		{
			$this->_scriptName = '/index.php';
		}
		$this->sanitizeURI($this->_scriptName);

		// split script_name into urlBase/scriptFile

		$splitPoint = strrpos($this->_scriptName, '/');
		$this->_scriptFile = substr($this->_scriptName, $splitPoint+1);
		$this->_urlBase = substr($this->_scriptName, 0, $splitPoint);

		$this->_urlPrefix = '';
		$this->_output = '';

		parent::__construct();
		
		// don't manufacture any plugs until we have constructed out parent!

		$this->_view = $this->_factory->manufacture('SparkView', array("{$this->_spDir}/views", "{$this->_appDir}/views"), empty($config['view_name_extension']) ? '' : "_{$config['view_name_extension']}");

		$this->init();
	}
	
	// --------------------------------------------------------------------------

   /**
    * Return the Spark/Plug directory path.
    *
    * @return string Path to Spark/Plug directory
    */
	final public function spDir()
	{
		return $this->_spDir;
	}

	// --------------------------------------------------------------------------

   /**
    * Return the application directory path.
    *
    * @return string Path to application directory
    */
	final public function appDir()
	{
		return $this->_appDir;
	}

	// --------------------------------------------------------------------------

   /**
    * Push a view directory onto the stack. Calls through to view's pushViewDir method.
    *
    * @param string $dir Path to view directory
    */
	final public function pushViewDir($dir)
	{
		$this->_view->pushViewDir($dir);
	}

	// --------------------------------------------------------------------------

   /**
    * Pop the view directory stack. Calls through to view's popViewDir method.
    *
    */
	final public function popViewDir()
	{
		return $this->_view->popViewDir();
	}

	// --------------------------------------------------------------------------

   /**
    * Push a model directory onto the stack.
    *
    * @param string $dir Path to model directory
    */
	final public function pushModelDir($dir)
	{
		$this->_modelDirStack[] = $dir;
	}

	// --------------------------------------------------------------------------

   /**
    * Pop the model directory stack.
    *
    */
	final public function popModelDir()
	{
		return array_pop($this->_modelDirStack);
	}

	// --------------------------------------------------------------------------

   /**
    * Reset the model directory stack.
    *
    */
	final public function resetModelDirStack($dir = NULL)
	{
		$this->_modelDirStack = array($dir ? $dir : $this->_modelDirStack[0]);
	}

	// --------------------------------------------------------------------------

   /**
    * Push a controller directory onto the stack.
    *
    * @param string $dir Path to controller directory
    */
	final public function pushControllerDir($dir)
	{
		$this->_controllerDirStack[] = $dir;
	}

	// --------------------------------------------------------------------------

   /**
    * Pop the controller directory stack.
    *
    */
	final public function popControllerDir()
	{
		return array_pop($this->_controllerDirStack);
	}

	// --------------------------------------------------------------------------

   /**
    * Reset the controller directory stack.
    *
    */
	final public function resetControllerDirStack($dir = NULL)
	{
		$this->_controllerDirStack = array($dir ? $dir : $this->_controllerDirStack[0]);
	}

	// --------------------------------------------------------------------------

   /**
    * Run the application!
    *
    */
	public function run()
	{
		// sanitize the input data, remove slashes added if magic_quotes mode was on for gpc data
		
		$this->sanitize(get_magic_quotes_gpc());

		try
		{
			// check for overriding request method
	
			if ($this->_emulateHTTPMethods && ($this->_requestMethod === SparkUtil::kRequestMethod_POST))
			{
				if (($method = @$_POST['_method']) || ($method = @$_GET['_method']))
				{
					unset($_POST['_method']);
					unset($_GET['_method']);
					if (!$this->_requestMethod = SparkUtil::get_http_method($method))
					{
						throw new SparkHTTPException_NotImplemented();
					}
				}
			}
	
			$this->observer->notify('SparkApplication:run:before');
			$this->dispatch();
			$this->observer->notify('SparkApplication:run:after');

			if (!empty($this->_output))
			{
				$this->observer->notify('SparkApplication:display:before', $this->_output);
				$this->display($this->_output);
				$this->observer->notify('SparkApplication:display:after', $this->_output);
			}
		}
		catch (SparkHTTPException_Unauthorized $e)
		{
			if ($this->config->get('http_auth_basic_enabled'))
			{
				$realm = $this->config->get('http_auth_basic_realm');
				header('WWW-Authenticate: Basic realm="' . $realm . '"');
			}
			self::setStatus($e->getHTTPStatus());
			$this->display($this->render('error_'.$e->getHTTPStatusCode() . ',error_http', $e->vars() + array('status'=>$e->getHTTPStatus(), 'message'=>$e->getMessage(), 'exception' => $e)));
			$this->observer->notify('SparkApplication:run:exception:' . get_class($e), $e);
		}
		catch (SparkHTTPException $e)
		{
			self::setStatus($e->getHTTPStatus());
			$this->display($this->render('error_'.$e->getHTTPStatusCode() . ',error_http', $e->vars() + array('status'=>$e->getHTTPStatus(), 'message'=>$e->getMessage(), 'exception' => $e)));
			$this->observer->notify('SparkApplication:run:exception:' . get_class($e), $e);
		}
		catch (SparkException $e)
		{
			self::setStatus('500 Internal Server Error');
			$this->display($this->render('error_500,error_http', $e->vars() + array('status'=>'500 Internal Server Error', 'message'=>$e->getMessage(), 'exception' => $e)));
			$this->observer->notify('SparkApplication:run:exception:' . get_class($e), $e);
		}
		catch (Exception $e)
		{
			self::setStatus('500 Internal Server Error');
			$this->display($this->render('error_500,error_http', array('status'=>'500 Internal Server Error', 'message'=>$e->getMessage(), 'exception' => $e)));
			$this->observer->notify('SparkApplication:run:exception', $e);
		}
	}

	// --------------------------------------------------------------------------

   /**
    * Return the object factory
    *
    * @return object Object factory object
    */
	public function factory()
	{
		return $this->_factory;
	}

	// --------------------------------------------------------------------------

   /**
    * Return the application config object
    *
    * @return object Configuration object
    */
	public function config()
	{
		return $this->_config;
	}

	// --------------------------------------------------------------------------

   /**
    * Return the application view object
    *
    * @return object View object
    */
	public function view()
	{
		return $this->_view;
	}

	// --------------------------------------------------------------------------

   /**
    * Render a view file. Calls through to view's render method.
    * Accepts 0, 1, 2 or 3 parameters. Parameters may appear in any order and are as follows:
    *
    * @param string $viewList Comma-delimited array of names of views to render (first found will be rendered)
    * @param array $vars Array of variables to make available to view (extract into view as locals)
    * @param bool $returnBuffer true if rendered result should be returned instead of buffered for display
    * @return string Result of render (if $returnBuffer is true)
    */
	public function render()
	{
		$returnBuffer = false;
		
		$args = func_get_args();

		foreach ($args as $arg)
		{
			if (is_bool($arg))
			{
				$returnBuffer = $arg;
				break;
			}
		}
		
		$out = call_user_func_array(array($this->_view, 'render'), $args);

		if (!$returnBuffer)
		{
			$this->_output .= $out;
		}

		return $out;
	}

	// --------------------------------------------------------------------------

   /**
    * Render an exception page for the specified exception.
    * This is a low-level function that should rarely be called.
    * Useful to gracefully handle exceptions that occur before the application has been fully instantiated.
    *
    * @param Exception $e Exception to render
    */
	public function showExceptionPage($e)
	{
		$vars = ($e instanceof SparkException) ? $e->vars() : array();
		
		if ($e instanceof SparkHTTPException)
		{
			$status = $e->getHTTPStatus();
			$viewList = 'error_'.$e->getHTTPStatusCode() . ',error_http';
		}
		else
		{
			$status = '500 Internal Server Error';
			$viewList = 'error_500,error_http';
		}
		
		self::setStatus($status);
		$this->display($this->render($viewList, $vars + array('status'=>$status, 'message'=>$e->getMessage(), 'exception' => $e), true));
		exit;
	}
	
	// --------------------------------------------------------------------------

   /**
    * Send output to the browser.
    *
    * @param string $output Display-ready content to send to browser
    * @param string $contentType Content type to send to browser
    * @param string $status HTTP status code to set in header (optional)
    * @param array $headers array of extra headers to send (optional)
    * @return string Output is also returned to caller
    */
	public function display($output, $contentType = 'text/html', $status = NULL, $headers = NULL)
	{
		if ($status !== NULL)
		{
			self::setStatus($status);
		}
		if ((stripos($contentType, 'text/') === 0) && (stripos($contentType, 'charset') === false))
		{
			$contentType .= "; charset={$this->_charset}";
		}
		header("Content-Type: {$contentType}");
		if (!empty($headers))
		{
			foreach ($headers as $key => $val)
			{
				header("{$key}: {$val}");
			}
		}
		echo $output;
		return $output;
	}

	// --------------------------------------------------------------------------

   /**
    * Send download to the browser.
    *
    * @param string $output Content to download to browser
    * @param string $contentType Content type to send to browser
    * @param string $fileName Name of file we're sending to browser
    * @param int $fileSize Size of file we're sending to browser
    */
	public function download($output, $contentType, $fileName, $fileSize)
	{
		header("Content-Type: {$contentType}");
		header("Content-Disposition: attachment; filename=\"{$fileName}\"; size=\"{$fileSize}\"");
		header("Content-Length: {$fileSize}");
		header('Content-Description: File Download');
		header('Content-Transfer-Encoding: binary');
		header('Cache-Control: private');
		echo $output;
	}

	// --------------------------------------------------------------------------

   /**
    * Create a new model object.
    *
    * @param string $name Name of model class to instatiate
    * @return object Model object
    */
	public function newModel($name, $params = NULL)
	{
		// all model classes have "Model" suffix on name

		$class = "{$name}Model";

		if (!class_exists($class, false))
		{
			$modelFileName = SparkInflector::decamelize($name) . $this->_modelNameExtension . '.php';
			for ($iModelDir = count($this->_modelDirStack) - 1; $iModelDir >= 0; --$iModelDir)
			{
				if (file_exists($modelPath = "{$this->_modelDirStack[$iModelDir]}/{$modelFileName}"))
				{
					require($modelPath);
					break;
				}
			}
		}

		return $this->_factory->manufacture($class, $params);
	}

	// --------------------------------------------------------------------------

   /**
    * Create a full url to the specified (static) path.
    *
    * @param string $uri Path to which to create full URL
    * @param bool $withHost whether to include host in URL
    * @param bool $withScheme whether to include scheme in URL
    * @return string URL
    */
	public function urlToStatic($uri, $withHost = false, $withScheme = true)
	{
		if ($withScheme === true)
		{
			$scheme = SparkUtil::scheme();
		}
		elseif (is_string($withScheme))
		{
			$scheme = $withScheme . '://';
		}
		else
		{
			$scheme = '';
		}

		if ($withHost === true)
		{
			$host = ($scheme === 'https://') ? $this->_appHostSecure : $this->_appHost;
		}
		elseif (is_string($withHost))
		{
			$host = $withHost;
		}
		else
		{
			$host = '';
		}

		return ($withHost ? $scheme.$host : '') . $this->_urlBase . $uri;
	}

	// --------------------------------------------------------------------------

   /**
    * Create a full url to the specified (dynamic) path.
    *
    * @param string $uri Path to which to create full URL
    * @param bool $withHost whether to include host in URL
    * @param bool $withScheme whether to include scheme in URL
    * @return string URL
    */
	public function urlTo($uri, $withHost = false, $withScheme = true)
	{
		if ($withScheme === true)
		{
			$scheme = SparkUtil::scheme();
		}
		elseif (is_string($withScheme))
		{
			$scheme = $withScheme . '://';
		}
		else
		{
			$scheme = '';
		}
		
		if ($withHost === true)
		{
			$host = ($scheme === 'https://') ? $this->_appHostSecure : $this->_appHost;
		}
		elseif (is_string($withHost))
		{
			$host = $withHost;
		}
		else
		{
			$host = '';
		}
		
		return ($withHost ? $scheme.$host : '') . $this->_urlPrefix . $uri;
	}

	// --------------------------------------------------------------------------

   /**
    * Return current request method.
    *
    * @return in Request method
    */
	public function getRequestMethod()
	{
		return $this->_requestMethod;
	}

	// --------------------------------------------------------------------------

   /**
    * Return a supported MIME type given its extension.
    *
    * @param string $ext Media type extension
    * @return string Media type
    */
	public function getMediaType($ext)
	{
		return $this->_supportedTypes[$ext];
	}

	// --------------------------------------------------------------------------

   /**
    * Return a supported media extension given its MIME type.
    *
    * @param string $ext Media type extension
    * @return string Media type
    */
	public function getMediaExtension($type)
	{
		return $this->_iSupportedTypes[$type];
	}

	// --------------------------------------------------------------------------

   /**
    * Encode response data according to its content type.
    *
    * @param string $data Response data string
    * @param string $contentType HTTP Content-Type
    * @return string Returns ancoded data as a string
    */
	public function encodeResponseData($data, $contentType)
	{
		if (empty($data))
		{
			return;
		}
		
		if ($contentType === 'application/x-www-form-urlencoded')
		{
			$method = 'encode_uri';
		}
		
		else
		{
			if ((!$extension = @$this->_iSupportedTypes[$contentType]) || !method_exists($this, $method = 'encode_' . $extension))
			{
				throw new SparkHTTPException_UnsupportedMediaType();
			}
		}

		return $this->{$method}($data);
	}

	// --------------------------------------------------------------------------

   /**
    * Encode response data into application/x-www-form-urlencoded format.
    *
    * @param any $data Object to encode
    * @param string Returns application/x-www-form-urlencoded representation
    */
	protected function encode_uri($data, $prefix = NULL)
	{
		$encoded = '';
		
		foreach ((array)$data as $key => $value)
		{
			if ($prefix)
			{
				if (is_int($key))
				{
					$key = $prefix . '[]';
				}
				else
				{
					$key = $prefix . "[{$key}]";
				}
			}
			if (!empty($encoded))
			{
				$encoded .= '&';
			}
			if (is_array($value))
			{
				$encoded .= self::encode_uri($value, $key);
			}
			else
			{
				$encoded .= ($key . '=' . SparkView::escape_uri($value));
			}
		}
		
		return $encoded;
	}
	
	// --------------------------------------------------------------------------

   /**
    * Encode response data into XML format.
    *
    * @param any $data Object to encode
    * @param string Returns XML-encoded representation
    */
	protected function encode_xml($data)
	{
		return $this->encode_xml_node(simplexml_load_string("<?xml version='1.0' encoding='utf-8'?\x3E<xml />"), $data)->asXML();
	}
	
	protected function encode_xml_node($node, $data)
	{
		foreach((array)$data as $key => $value)
		{
			if (is_numeric($key))	// disallow numeric keys
			{
				$key = 'item_' . $key;
			}
			
			$key = preg_replace('/[^a-z0-9_-]/i', '', $key);
			
			if (is_array($value) || is_object($value))
			{
				$this->encode_xml_node($node->addChild($key), $value);
			}
			else
			{
				$node->addChild($key, SparkView::escape_xml($value));
			}
		}
		
		return $node;
	}
	
	// --------------------------------------------------------------------------

   /**
    * Encode response data into JSON format.
    *
    * @param any $data Object to encode
    * @param string Returns JSON-encoded representation
    */
	protected function encode_json($data)
	{
		return json_encode($data);
	}
	
	// --------------------------------------------------------------------------

   /**
    * Encode response data into PHPS format.
    *
    * @param any $data Object to encode
    * @param string Returns PHPS-encoded representation
    */
	protected function encode_phps($data)
	{
		return serialize($data);
	}

	// --------------------------------------------------------------------------

   /**
    * Encode response data into HTML format (as a table).
    *
    * @param any $data Object to encode
    * @param string Returns HTML-encoded representation
    */
	protected function encode_html($data)
	{
		$output = '<html><head></head><body>';
		$output .= $this->encode_html_table($data);
		$output .= '</body></html>';
		return $output;
	}

	protected function encode_html_table($data)
	{
		if (is_array(@$data[0]))
		{
			$headings = array_keys($data[0]);
		}
		else
		{
			$headings = array_keys($data);
			$data = array($data);
		}
		
		$output = '<table><thead><tr><th>' . implode('</th><th>', array_map(array('SparkView', 'escape_html'), $headings)) . '</th></td></thead><tbody>' . "\n";

		foreach ($data as $row)
		{
			$output .= '<tr>';

			foreach ($row as $val)
			{
				$output .= '<td>' . (is_array($val) ? $this->encode_html_table($val) : SparkView::escape_html($val)) . '</td>';
			}
			
			$output .= "</tr>\n";
		}
		
		$output .= '</tbody></table>';
		
		return $output;
	}

	// --------------------------------------------------------------------------

   /**
    * Encode response data into TEXT format.
    *
    * @param any $data Object to encode
    * @param string Returns TEXT-encoded representation
    */
	protected function encode_txt($data)
	{
		if (is_array(@$data[0]))
		{
			$headings = array_keys($data[0]);
		}
		else
		{
			$headings = array_keys($data);
			$data = array($data);
		}
		
		$output = implode(' ', $headings) . "\n";

		foreach ($data as $row)
		{
			foreach ($row as $val)
			{
				$output .= ' ' . (is_array($val) ? $this->encode_txt($val) : $val);
			}
			$output .= "\n";
		}
		
		return $output;
	}

	// --------------------------------------------------------------------------

   /**
    * Encode response data into CSV format.
    *
    * @param any $data Object to encode
    * @param string Returns CSV-encoded representation
    */
	protected function encode_csv($data)
	{
		if (is_array(@$data[0]))
		{
			$headings = array_keys($data[0]);
		}
		else
		{
			$headings = array_keys($data);
			$data = array($data);
		}
		
		$output = implode(',', $headings) . "\r\n";

		foreach ($data as $row)
		{
			$cols = array();
			foreach ($row as $val)
			{
				$cols[] = is_array($val) ? 'nested data' : $val;
			}
			
			$output .= '"' . implode('","', $cols) . "\"\r\n";
		}
		
		return $output;
	}

	// --------------------------------------------------------------------------
	
   /**
    * Get the application ready to go.
    *
    */
	protected function init()
	{
		if (ini_get('register_globals'))
		{
			self::killGlobals();
		}
		
		// take care of some PHP config

		if (defined('E_RECOVERABLE_ERROR'))
		{
			set_error_handler(array('SparkPHPException', 'errorHandler'), E_RECOVERABLE_ERROR);
		}
		date_default_timezone_set($this->_config->get('timezone', 'UTC'));

		// set language and encoding

		$language = $this->_config->get('language', 'en_US');
		$charset2 = strtolower(str_replace('-', '', $this->_charset));

		setlocale(LC_ALL, array("{$language}.{$this->_charset}", "{$language}.{$charset2}", $language));
		ini_set('default_charset', $this->_charset);
		if (function_exists('mb_http_output'))
		{
			mb_http_output($this->_charset);
			mb_internal_encoding($this->_charset);
		}
	}

	// --------------------------------------------------------------------------

   /**
    * Sanitize GPC data
    *
    * @param boolean $doStrip true -> remove slashes
    */
	protected function sanitize($doStrip = false)
	{
		$this->sanitizeData($_GET, $doStrip);
		$this->sanitizeData($_POST, $doStrip);
		$this->sanitizeData($_COOKIE, $doStrip);
	}
	
	// --------------------------------------------------------------------------

   /**
    * Sanitize a string or recursively sanitize an array.
    *
    * @param string|array &$item Data item to sanitize (reference)
    * @param boolean $doStrip true -> remove slashes
    */
	protected function sanitizeData(&$item, $doStrip = false)
	{
		if (is_array($item))
		{
			foreach ($item as $key => $val)
			{
				if (!preg_match("/^[a-z0-9:_\/-]+$/i", $key))
				{
					throw new SparkHTTPException_BadRequest(NULL, array('reason'=>'disallowed characters in input'));
				}
				$this->sanitizeData($item[$key], $doStrip);
			}
		}
		elseif (is_string($item))
		{
			$this->sanitizeString($item, $doStrip);
		}
	}
	
	// --------------------------------------------------------------------------

   /**
    * Sanitize a string.
    *
    * @param string &$item Data item to sanitize (reference)
    * @param boolean $doStrip true -> remove slashes
    */
	protected function sanitizeString(&$item, $doStrip = false)
	{
		if ($doStrip)
		{
			$item = stripslashes($item);
		}
		$item = trim(str_replace(array("\r\n", "\r"), "\n", $item));
	}
	
	// --------------------------------------------------------------------------

   /**
    * Dispatch to controller/action selected by URL.
    * The first URL segment following the URL prefix selects the controller (class),
    * and the second segment selects the action (method).
    *
    */
	protected function dispatch()
	{
		// parse the uri

		if ($this->_RESTful)
		{
			$this->parseRESTfulURI($controller, $action, $params);
		}
		else
		{
			$this->parseURI($controller, $action, $params);
		}

		$params['base_uri'] = "/{$controller}/{$action}";	// for controller's convenience

		if (!isset($params['qv']))
		{
			$params['qv'] = $_GET;
		}
		if (!isset($params['pv']))
		{
			$params['pv'] = $_POST;
		}
		if (!isset($params['cv']))
		{
			$params['cv'] = $_COOKIE;
		}
		$params['kv'] = array_merge($params['qv'], $params['pv'], $params['rv']);

		$params['auth']['user'] = @$_SERVER['PHP_AUTH_USER'];
		$params['auth']['pw'] = @$_SERVER['PHP_AUTH_PW'];

		// enforce consistent access to GPC data
		
		unset($_GET); unset($GLOBALS['HTTP_GET_VARS']);
		unset($_POST); unset($GLOBALS['HTTP_POST_VARS']);
		unset($_COOKIE); unset($GLOBALS['HTTP_COOKIE_VARS']);

		// set default view
		
		$this->_view->setDefault(trim($params['base_uri'], '/'));

		// all controller classes have "Controller" suffix on name
		// all controller action methods have "action_" prefix, which simplifies some security issues

		$class = SparkInflector::camelize($controller) . 'Controller';
		$method = 'action_' . str_replace('-', '_', $action);

		// load the controller

		if (!class_exists($class, false))
		{
			$controllerFilename = $controller . $this->_controllerNameExtension . '.php';
			for ($iControllerDir = count($this->_controllerDirStack) - 1; $iControllerDir >= 0; --$iControllerDir)
			{
				if (file_exists($controllerPath = "{$this->_controllerDirStack[$iControllerDir]}/{$controllerFilename}"))
				{
					require($controllerPath);
					break;
				}
			}
		}
		
		// instantiate controller
		
		try
		{
			$controller = $this->_factory->manufacture($class, $this);
		}
		catch (Exception $e)
		{
			$controller = NULL;
		}

		// did we successfully instantiate the controller?

		if (($controller === NULL) || !($controller instanceof _SparkController))
		{
			throw new SparkHTTPException_NotFound(NULL, array('reason'=>'no controller'));
		}
		
		// sanity checks

		if (!method_exists($controller, $method))
		{
			throw new SparkHTTPException_NotFound(NULL, array('reason'=>"method not found: {$method}"));
		}

		// if the controller has a pre-dispatch method, call it and pass the method we're about to invoke
		
		if (!$controller->_before_dispatch($method, $params))
		{
			throw new SparkHTTPException_BadRequest(NULL, array('reason'=>'controller rejected request'));
		}

		// send pre_dispatch notification
		
		$this->observer->notify('SparkApplication:dispatch:before:' . $class . ':' . $method, $class, $method, $controller, $params);

		// call controller to generate page

		$controller->{$method}($params);

		// send post_dispatch notification
		
		$this->observer->notify('SparkApplication:dispatch:after:' . $class . ':' . $method, $class, $method, $controller, $params);
	}

	// --------------------------------------------------------------------------

   /**
    * Decompose the URI into its components:
    *   controller (class), action (method), and parameters.
    *
    * @param object &$controller Returns name of selected controller class
    * @param object &$action Data Returns name of selected controller action
    * @param array &$params Returns action parameters
    */
	protected function parseURI(&$controller, &$action, &$params)
	{
		// Get the URI (the remainder of the URL after removing the host and scheme)
		// and break it into its component segments.
		
		$params = explode('/', trim($this->getURI($vars, $overrides), '/'));

		// first segement is controller (may be NULL for default)

		if ($controller = @$overrides['controller'])
		{
			array_shift($params);
		}
		elseif (!$controller = array_shift($params))
		{
			$controller = $this->_config->get('default_controller', 'default');
		}

		// second segement is action (may be NULL for default)

		if ($action = @$overrides['action'])
		{
			array_shift($params);
		}
		elseif (!$action = array_shift($params))
		{
			$action = $this->_config->get('default_action', 'index');
		}

		if (!empty($overrides['action_extra']))
		{
			$action .= $overrides['action_extra'];
		}

		$segments = $params;

		// the remaining segements are parameters passed to the controller action,
		// and we add in GPC data as well

		$params['count'] = count($params);						// for controller's convenience
		$params['segments'] = $segments;							// duplicate main params here for controller's convenience
		$params['rv'] = $vars;
		$params['get'] =& $_GET;									// deprecated
		$params['post'] =& $_POST;									// deprecated
		$params['cookie'] =& $_COOKIE;							// deprecated
	}
	
	// --------------------------------------------------------------------------

   /**
    * Decompose a RESTful URI into its components:
    *   controller (class), action (method), and parameters.
    *
    * @param object &$controller Returns name of selected controller class
    * @param object &$action Data Returns name of selected controller action
    * @param array &$params Returns action parameters
    */
	protected function parseRESTfulURI(&$controller, &$action, &$params)
	{
		// Get the URI (the remainder of the URL after removing the host and scheme)
		// and break it into its component segments.
		
		$segments = explode('/', trim($this->getURI($vars, $overrides), '/'));
		$extension = '';

		// The resource type and optional ID are the final two components.

		if (count($segments) > 1)
		{
			$resourceID = array_pop($segments);
			
			if (($pos = strrpos($resourceID, '.')) !== false)
			{
				$extension = substr($resourceID, $pos + 1);
				$resourceID = substr($resourceID, 0, $pos);
			}
			
			$params['id'] = $resourceID;
		}
		if (!$controller = @$overrides['controller'])
		{
			$controller = array_pop($segments);
		}
		
		if (!isset($resourceID))
		{
			if (($pos = strrpos($controller, '.')) !== false)
			{
				$extension = substr($controller, $pos + 1);
				$controller = substr($controller, 0, $pos);
			}
		}
		if (!isset($this->_supportedTypes[$extension]))
		{
			throw new SparkHTTPException_UnsupportedMediaType();
		}

		$params['resource'] = strtolower($controller);
		$contentType = $this->_supportedTypes[$extension];
		
		// determine the REST action to take on the resource

		if (!$action = @$overrides['action'])
		{
			switch ($this->_requestMethod)
			{
				case SparkUtil::kRequestMethod_HEAD:
					$params['head_only'] = true;
				case SparkUtil::kRequestMethod_GET:
					$action = isset($resourceID) ? 'show' : 'index';
					break;
					
				case SparkUtil::kRequestMethod_POST:
					$action = isset($resourceID) ? 'append' : 'create';
					break;
					
				case SparkUtil::kRequestMethod_PUT:
					$action = 'update';
					break;
					
				case SparkUtil::kRequestMethod_DELETE:
					$action = isset($resourceID) ? 'destroy' : 'clear';
					break;
					
				case SparkUtil::kRequestMethod_OPTIONS:
					$action = 'options';
					break;
			}
		}
		
		if (!empty($overrides['action_extra']))
		{
			$action .= $overrides['action_extra'];
		}
		
		// determine acceptable content types for response
		// if an explicit extension was provided in the URI (eg. ".json"), then use that
		// otherwise, use accept headers
		
		$params['http-accept'] = array();
		
		if (empty($extension))
		{
			foreach (explode(',', @$_SERVER['HTTP_ACCEPT']) as $mediaType)
			{
				$mediaType = (($pos = strpos($mediaType, ';')) === false ? $mediaType : substr($mediaType, 0, $pos));
				if (isset($this->_iSupportedTypes[$mediaType]))
				{
					$params['http-accept'][] = $mediaType;
				}
			}
		}
		$params['http-accept'][] = $contentType;

		if ($this->_requestMethod === SparkUtil::kRequestMethod_GET)
		{
			$params['request'] = SparkUtil::query_string();
			$params['pv'] = array();
		}
		else
		{
			// determine content type of request
			// if an explicit extension was provided in the URI (eg. ".json"), then we use that
			// otherwise, if a CONTENT_TYPE header is present, we use that
			// otherwise, we use the default from in the config file (specified by an empty key in the 'supported_types' array)

			if (!empty($extension))
			{
				$params['http-content-type'] = $contentType;
			}
			else
			{
				$contentTypeHeader = explode(';', $_SERVER['CONTENT_TYPE']);
				$params['http-content-type'] = !empty($contentTypeHeader[0]) ? $contentTypeHeader[0] : '';
			}
			$params['request'] = file_get_contents('php://input');
			$this->decodeRequestData($params['request'], $params['http-content-type'], $params['pv']);
		}
		
		$params['count'] = 0;
		$params['segments'] = NULL;								
		$params['rv'] = $vars;
	}
	
	// --------------------------------------------------------------------------

   /**
    * Decode the request data according to its content type into the output array.
    *
    * @param string $request Request data string
    * @param string $contentType HTTP Content-Type
    * @param array &$data Returns decoded data as array of key-value pairs
    */
	protected function decodeRequestData($request, $contentType, &$data)
	{
		if ($contentType === 'application/x-www-form-urlencoded')
		{
			$method = 'decode_uri';
		}
		
		else
		{
			if ((!$extension = @$this->_iSupportedTypes[$contentType]) || !method_exists($this, $method = 'decode_' . $extension))
			{
				throw new SparkHTTPException_UnsupportedMediaType();
			}
		}

		$this->{$method}($request, $data);
		$this->sanitizeData($data, get_magic_quotes_gpc());
		
		if (!is_array($data))
		{
			$data = array();
		}
	}

	// --------------------------------------------------------------------------

   /**
    * Decode the application/x-www-form-urlencoded request data.
    *
    * @param string $request Request data string
    * @param array &$data Returns decoded data as array of key-value pairs
    */
	protected function decode_uri($request, &$data)
	{
		if ($this->_requestMethod === SparkUtil::kRequestMethod_POST)
		{
			$data = $_POST;
		}
		else
		{
			parse_str($request, $data);
		}
	}
	
	// --------------------------------------------------------------------------

   /**
    * Decode the XML request data.
    *
    * @param string $request Request data string
    * @param array &$data Returns decoded data as array of key-value pairs
    */
	protected function decode_xml($request, &$data)
	{
		$data = $this->convertSimpleXMLElementToArray(simplexml_load_string($request));
	}

	protected function convertSimpleXMLElementToArray($element)
	{
		if ($element instanceof SimpleXMLElement)
		{
			$element = (array) $element;
		}
		if (is_array($element))
		{
			foreach ($element as $key => $val)
			{
				$element[$key] = self::convertSimpleXMLElementToArray($val);
			}
		}
		return $element;
	}
	
	// --------------------------------------------------------------------------

   /**
    * Decode the JSON request data.
    *
    * @param string $request Request data string
    * @param array &$data Returns decoded data as array of key-value pairs
    */
	protected function decode_json($request, &$data)
	{
		$data = json_decode($request, true);
	}
	
	// --------------------------------------------------------------------------

   /**
    * Decode the PHPS request data.
    *
    * @param string $request Request data string
    * @param array &$data Returns decoded data as array of key-value pairs
    */
	protected function decode_phps($request, &$data)
	{
		$data = (array)unserialize($request);
	}
	
	// --------------------------------------------------------------------------

   /**
    * Decode the HTML request data.
    *
    * @param string $request Request data string
    * @param array &$data Returns decoded data as array of key-value pairs
    */
	protected function decode_html($request, &$data)
	{
		$data['content'] = $request;
	}
	
	// --------------------------------------------------------------------------

   /**
    * Decode the TEXT request data.
    *
    * @param string $request Request data string
    * @param array &$data Returns decoded data as array of key-value pairs
    */
	protected function decode_txt($request, &$data)
	{
		$data['content'] = $request;
	}
	
	// --------------------------------------------------------------------------

   /**
    * Decode the CSV request data.
    *
    * @param string $request Request data string
    * @param array &$data Returns decoded data as array of key-value pairs
    */
	protected function decode_csv($request, &$data)
	{
		$csv = preg_split('/\r?\n/', $request);
		$header = str_getcsv(array_shift($csv));
		foreach ($csv as $row)
		{
			$data[] = array_combine($header, str_getcsv($row));
		}
	}
	
	// --------------------------------------------------------------------------

   /**
    * Dissect the URL and extract the URL base, URL prefix (if any) and URI.
    * Sanitize the URI and run it through the routing table to determine actual URI
    * used for dispatching.
    *
    * @param array &$vars Returns hash of extracted URI variables
    * @param array &$overrides Returns controller/action overrides, if any
    * @return string URI
    */
	protected function getURI(&$vars, &$overrides)
	{
		// get URI minus any query parameters
		
		$requestURI = preg_replace('/\?.*$/', '', SparkUtil::request_uri());
		$this->sanitizeURI($requestURI);
		
		// Are we including the index file in URLs we generate? If the user did not specify, make a best guess based on the contents
		// of the request URI. Note that this test won't work for the home page if the index file is not specified in the URI.
		
		if (($useIndexFile = $this->_config->get('use_index_file')) === NULL)
		{
			$useIndexFile = (stripos($requestURI, $this->_scriptName) === 0);
		}
		
		// determine the urlPrefix
		
		$this->_urlPrefix = $useIndexFile ? $this->_scriptName : $this->_urlBase;

		// if server supports PATH_INFO, that is the easiest way to extract the URI
		
		$uri_protocol = strtoupper($this->_config->get('uri_protocol', 'PATH_INFO'));

		if (!$uri = isset($_SERVER[$uri_protocol]) ? $_SERVER[$uri_protocol] : @getenv($uri_protocol))
		{
			$scriptNameLen = strlen($this->_scriptName);

			// only replace from start of string!

			if (stripos($requestURI, $this->_scriptName) !== 0)		// if $this->_scriptName does not appear at start of $requestURI...
			{
				if (stripos($this->_scriptName, '/'.$this->_scriptFile) === ($len = $scriptNameLen - strlen($this->_scriptFile) - 1))
				{
					$scriptNameLen = $len;
				}
			}
			
			if (($uri = substr($requestURI, $scriptNameLen)) === false)
			{
				$uri = '/';
			}
		}

		// sanitize
		
		$this->sanitizeURI($uri);

		// run resulting URI through routing table to pick up any custom routing

		return $this->routeURI($uri, $vars, $overrides);
	}
	
	// --------------------------------------------------------------------------

   /**
    * Check URI for dangerous and disallowed characters, then clean it up.
    *
    * @param string &$uri URI to filter
    */
	protected function sanitizeURI(&$uri)
	{
		// filter a URI string for dangerous characters

		 if (!preg_match('/^[a-z0-9~\s\.\/;,:_-]*$/i', $uri))
		 {
			throw new SparkHTTPException_BadRequest(NULL, array('reason'=>'disallowed characters in URL'));
		 }
		 
		// reduce multiple consecutive slashes to a single slash
		// remove leading and trailing white space
		// convert to lower case

		 $uri = strtolower(trim(preg_replace('/\/+/', '/', $uri)));
	}
	
	// --------------------------------------------------------------------------

   /**
    * Run URI through routing table to pick up any custom routing.
    *
    * @param string $uri URI to route
    * @param array &$vars Returns hash of extracted URI variables
    * @param array &$overrides Returns controller/action overrides, if any
    * @return string URI to use for dispatching
    */
	protected function routeURI($uri, &$vars, &$overrides)
	{
		$vars = array();
		$overrides = NULL;

		$routeExtra = '';
		$matched = false;
		
		foreach ($this->_config->get('routes', array()) as $pattern => $route)
		{
			if (is_array($target = $route))
			{
				if (!$target = @$target['uri'])
				{
					$target = $pattern;
				}
			}
			
			// convert routes with wildcards to regex syntax
		
			if (strpos($pattern, ':') !== false)
			{
				$pattern = str_replace(array(':slug', ':name', ':id', ':hexid', ':word', ':alnum', ':alpha', ':num', ':hex', ':any', ':all'), array('([\w\-\.\,\;]+)', '((?:[a-z])[\w\-\.]+?)', '(\d+(?:\.[a-z]+)?)', '([\da-f]+(?:\.[a-z]+)?)', '(\w+)', '([a-z0-9]+)', '([a-z]+)', '(\d+)', '([\da-f]+)', '([^/]+)', '((?:[\w\-\.\,\;]+/?)*)'), $pattern);
			}

			// simple case (literal pattern matching)
			
			if (strpos($pattern, '(') === false)
			{
				if (strpos($uri, $pattern) === 0)
				{
					$uri = str_replace($pattern, $target, $uri);
					$matched = true;
					break;
				}
			}
			
			// complex case (regex pattern matching)
			
			else if (preg_match("@^{$pattern}((?:/.*)?)$@i", $uri, $regex_matches))
			{
				unset($regex_matches[0]);
				
				$routeExtra = array_pop($regex_matches);

				// if the route uses back-references, build a back-reference array and perform the substitution

				if (strpos($target, '$') !== false)
				{
					$numBackRefs = count($regex_matches);
					for ($i = 1; $i <= $numBackRefs; ++$i)
					{
						$backRefs[] = "\$$i";
					}
					$target = str_replace($backRefs, $regex_matches, $target);
				}

				$uri = $target;
				$matched = true;
				break;
			}
		}
		
		// extract query variables, if any
		
		$parts = explode('?', $uri);
		$uri = $parts[0];
		
		// handle extra route data at end of uri
		
		if (!empty($routeExtra))
		{
			if ($this->_routeExtra)
			{
				$uri .= $routeExtra;
			}
			elseif ($routeExtra !== '/')
			{
				throw new SparkHTTPException_BadRequest(NULL, array('reason'=>'non-routable URI'));
			}
		}
		
		if (isset($parts[1]))
		{
			parse_str($parts[1], $vars);
		}
		
		// extract override controller/action, if present
		
		if ($matched && is_array($route))
		{
			if (!empty($route['controller']))
			{
				$overrides['controller'] = !empty($backRefs) ? str_replace($backRefs, $regex_matches, $route['controller']) : $route['controller'];
			}
			if (!empty($route['action']))
			{
				if (is_array($action = $route['action']))
				{
					$action = @$action[$this->_requestMethod];
				}
				if (!empty($action))
				{
					$overrides['action'] = !empty($backRefs) ? str_replace($backRefs, $regex_matches, $action) : $action;
				}
			}
			if (!empty($route['action_extra']))
			{
				$overrides['action_extra'] = !empty($backRefs) ? str_replace($backRefs, $regex_matches, $route['action_extra']) : $route['action_extra'];
			}
		}

		return $uri;
	}
}

// -----------------------------------------------------------------------------
/**
 * Compatibility Functions
 *
 */

// -----------------------------------------------------------------------------

if (!function_exists('str_getcsv'))
{
	function str_getcsv($input, $delimiter = ',', $enclosure = '"', $escape = '\\')
	{
		$fp = fopen('php://memory', 'r+');
		fputs($fp, $input);
		rewind($fp);
		$data = fgetcsv($fp, 0, $delimiter, $enclosure); // $escape only got added in 5.3.0
		fclose($fp);
		return $data;
	}
}

// -----------------------------------------------------------------------------

/**
 * Now that all essential classes have been defined, we can initialize the global
 * spark instance, loading all the core classes and their respective plugs.
*/

$spark->init();

// -----------------------------------------------------------------------------
